﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Linq;

namespace captcha_maker
{

    class BmpUtils
    {
        static  int PixelSize = 4;

        private struct ImagePixel
        {
            public bool belongsToChar;
            public int matchNConditions;
        }

        private class ColorCounter : IComparable<ColorCounter>
        {
            public Color color;
            public int quant;

            public ColorCounter(Color _color)
            {
                color = _color;
                quant = 1;
            }

            public int CompareTo(ColorCounter other)
            {
                if (other.quant == quant)
                    return 0;

                if (other.quant < quant)
                    return -1;

                return 1;
            }
        }

        public static byte[] GetBoolRepresentation(Bitmap _char)
        {
            bool [,] array = new bool [ _char.Height, _char.Width];


            // Find the background color
            BitmapData bmd = _char.LockBits(new Rectangle(0, 0, _char.Width, _char.Height), ImageLockMode.ReadWrite, _char.PixelFormat);
            unsafe
            {
                for (int y = 0; y < bmd.Height; y++)
                {
                    byte* row = (byte*)bmd.Scan0 + (y * bmd.Stride);

                    for (int x = 0; x < bmd.Width; x++)
                    {
                        Color curPixel = Color.FromArgb(row[x * PixelSize + 2], row[x * PixelSize + 1], row[x * PixelSize]);

                        array[y, x] = curPixel.ToArgb() != Color.White.ToArgb();
                        /*
                        if (array[y, x])
                        {
                            row[x * PixelSize + 2] = 0;
                            row[x * PixelSize + 1] = 0;
                            row[x * PixelSize ] = 0;
                        }*/

                    }
                }
            }
            _char.UnlockBits(bmd);

            // Converte o array bidimensional num unidimensional
            byte[] retArray = new byte[_char.Height * _char.Width];
            int i = 0;
            for (int l = 0; l < _char.Height; l++)
            {
                for (int c = 0; c < _char.Width; c++)
                {
                    retArray[i++] = array[l, c] == true ? (byte)1 : (byte)0;
                }
            }

            return retArray;
        }

        public static List<Bitmap> GetCaptchaChars(Bitmap _captcha, int _resWidth, int _resHeight)
        {
            
            Bitmap workBmp = new Bitmap(_captcha);

            // Find the background color
            List<ColorCounter> colors = new List<ColorCounter>();
            BitmapData bmd = workBmp.LockBits(new Rectangle(0, 0, workBmp.Width, workBmp.Height), ImageLockMode.ReadWrite, workBmp.PixelFormat);
            unsafe
            {
                for (int y = 0; y < bmd.Height; y++)
                {
                    byte* row = (byte*)bmd.Scan0 + (y * bmd.Stride);

                    for (int x = 0; x < bmd.Width; x++)
                    {
                        Color curPixel = Color.FromArgb(row[x * PixelSize + 2], row[x * PixelSize + 1], row[x * PixelSize]);

                        bool foundColor = false;

                        foreach (var colorCounter in
                            colors.Where(colorCounter => colorCounter.color.ToArgb() == curPixel.ToArgb()))
                        {
                            colorCounter.quant++;
                            foundColor = true;
                            break;
                        }

                        if (!foundColor)
                            colors.Add(new ColorCounter(curPixel));
                    }
                }
            }
            workBmp.UnlockBits(bmd);

            // Ordena as cores por quantidade
            colors.Sort();

            // Coloca o background a branco e os chars a preto
            workBmp = ClearColors(workBmp, colors[0].color);


            List<Bitmap> chars = new List<Bitmap>();
            int startX = 0;
            for (int i = 0; i < 4; i++)
            {
                Bitmap charBmp = ObtainChar(workBmp,ref startX);
                charBmp = BmpUtils.GetSkeleton(charBmp);
                charBmp = BmpUtils.ClipChar(charBmp, new Size(_resWidth, _resHeight));

                chars.Add(charBmp);
            }

            return chars;
        }


        private static Bitmap ObtainChar(Bitmap _bmp, ref int _startX)
        {
            Bitmap workBmp = new Bitmap(_bmp);


            // Aqui é assumido que o background já é branco e os chars têm cor preta

            bool foundStart = false;
            bool foundEnd = false;

            int curX = _startX;
            int localStartX = 0, localEndX = 0;

            // 
            // Procura o final do proximo char
            BitmapData bmd = workBmp.LockBits(new Rectangle(0, 0, workBmp.Width, workBmp.Height), ImageLockMode.ReadOnly, workBmp.PixelFormat);
            unsafe
            {
                do
                {
                    bool pixelOnColumn = false;

                    // Verifica uma coluna de cada vez
                    // 1º procura pelo fim do caracter
                    for (int y = 0; y < bmd.Height; y++)
                    {
                        byte* row = (byte*)bmd.Scan0 + (y * bmd.Stride);

                        Color curPixel = Color.FromArgb(row[curX * PixelSize + 2], row[curX * PixelSize + 1], row[curX * PixelSize]);

                        if (curPixel.ToArgb() != Color.White.ToArgb())
                        {
                            pixelOnColumn = true;
                            break;
                        }
                    }

                    if (pixelOnColumn && !foundStart)
                    {
                        // Inicio do char
                        localStartX = curX;
                        foundStart = true;
                    }

                    if (foundStart && !pixelOnColumn)
                    {
                        //  fim do char??
                        _startX = curX;
                        localEndX = curX - 1;
                        foundEnd = true;
                    }

                    curX++;

                } while (!foundEnd);
            }
            workBmp.UnlockBits(bmd);

            Rectangle clipRect = new Rectangle(localStartX, 0, ( localEndX - localStartX), workBmp.Height);
            Bitmap clipedBmp = ClipImage(workBmp, clipRect);

            return clipedBmp;
        }


        public static Bitmap ClearColors(Bitmap _bmp, Color _backColor)
        {
            Bitmap workBmp = new Bitmap(_bmp);

            const int PixelSize = 4;


            BitmapData bmd = workBmp.LockBits(new Rectangle(0, 0, workBmp.Width, workBmp.Height), ImageLockMode.ReadWrite, workBmp.PixelFormat);
            unsafe
            {
                for (int y = 0; y < bmd.Height; y++)
                {
                    byte* row = (byte*)bmd.Scan0 + (y * bmd.Stride);

                    for (int x = 0; x < bmd.Width; x++)
                    {
                        Color curPixel = Color.FromArgb(row[x * PixelSize + 2], row[x * PixelSize + 1], row[x * PixelSize]);

                        if (curPixel.ToArgb() == _backColor.ToArgb())
                        {
                            // Background pixel, set to White color
                            row[x * PixelSize + 2] = 255;
                            row[x * PixelSize + 1] = 255;
                            row[x * PixelSize] = 255;
                            continue;
                        }

                        // Char pixel, set to Black color
                        row[x * PixelSize + 2] = 0;
                        row[x * PixelSize + 1] = 0;
                        row[x * PixelSize] = 0;
                    }
                }
            }
            workBmp.UnlockBits(bmd);

            return workBmp;
        }

        public static Bitmap ClipChar(Bitmap _bmp, Size _clipSize)
        {
            Bitmap workBmp = new Bitmap(_bmp);

            const int PixelSize = 4;

            // Search for the most left, most top, most right, most bottom pixel
            //
            int[] corners = new[] { workBmp.Width, workBmp.Height, 0, 0 };

            BitmapData bmd = workBmp.LockBits(new Rectangle(0, 0, workBmp.Width, workBmp.Height), ImageLockMode.ReadWrite, workBmp.PixelFormat);
            unsafe
            {
                for (int y = 0; y < bmd.Height; y++)
                {
                    byte* row = (byte*)bmd.Scan0 + (y * bmd.Stride);

                    for (int x = 0; x < bmd.Width; x++)
                    {
                        Color curPixel = Color.FromArgb(row[x * PixelSize + 2], row[x * PixelSize + 1], row[x * PixelSize]);

                        if (curPixel.ToArgb() == Color.White.ToArgb()) continue;

                        // Update corners
                        if (y < corners[1]) corners[1] = y;
                        if (y > corners[3]) corners[3] = y;
                        if (x < corners[0]) corners[0] = x;
                        if (x > corners[2]) corners[2] = x;
                    }
                }
            }
            workBmp.UnlockBits(bmd);


            Rectangle clipRect = new Rectangle(corners[0], corners[1], (corners[2] - corners[0])+1, (corners[3] - corners[1])+1);
            Bitmap clipedBmp = ClipImage(workBmp, clipRect);

            // Resize
            return Resize(clipedBmp, _clipSize.Width, _clipSize.Height);
        }

        public static Bitmap ClipImage(Image _orig, Rectangle _clipRect)
        {
            Bitmap res = new Bitmap(_clipRect.Width+1, _clipRect.Height+1);
            Graphics g = Graphics.FromImage(res);
            g.Clear(Color.White);
            g.DrawImage(_orig, 0, 0, _clipRect, GraphicsUnit.Pixel);
            g.Flush();
            g.Dispose();

            return res;
        }

        public static Bitmap Resize(Image img, int _width, int _height)
        {
            //create a new Bitmap the size of the new image
            Bitmap bmp = new Bitmap(_width, _height);

            //create a new graphic from the Bitmap
            Graphics graphic = Graphics.FromImage(bmp);
            graphic.InterpolationMode = InterpolationMode.Bilinear;

            //draw the newly resized image
            graphic.DrawImage(img, 0, 0, _width, _height);

            //dispose and free up the resources
            graphic.Dispose();

            //return the image
            return bmp;
        }

        public static Bitmap AddBorder(Bitmap _bmp, int _size)
        {
            Bitmap res = new Bitmap(_bmp.Width + _size * 2, _bmp.Height + _size * 2);
            Graphics g = Graphics.FromImage(res);
            g.Clear(Color.White);

            Rectangle copyRect = new Rectangle(0, 0, _bmp.Width, _bmp.Height);
            g.DrawImage(_bmp, _size, _size, copyRect, GraphicsUnit.Pixel);
            g.Flush();
            g.Dispose();

            return res;
        }

        public static Bitmap RemoveBorder(Bitmap _bmp, int _size)
        {
            Bitmap res = new Bitmap(_bmp.Width - _size * 2, _bmp.Height - _size * 2);
            Graphics g = Graphics.FromImage(res);
            g.Clear(Color.White);

            Rectangle copyRect = new Rectangle(_size, _size, _bmp.Width - _size, _bmp.Height - _size);
            g.DrawImage(_bmp, 0, 0, copyRect, GraphicsUnit.Pixel);
            g.Flush();
            g.Dispose();

            return res;
        }

        public static Bitmap GetSkeleton(Bitmap _bmp)
        {
            const int PixelSize = 4;

            Bitmap workBmp = new Bitmap(_bmp);

            // Get a boolean representation of the bitmap
            ImagePixel[,] array = new ImagePixel[workBmp.Height, workBmp.Width];

            BitmapData bmd = workBmp.LockBits(new Rectangle(0, 0, workBmp.Width, workBmp.Height), ImageLockMode.ReadWrite,
                                           workBmp.PixelFormat);
            unsafe
            {
                for (int y = 0; y < bmd.Height; y++)
                {
                    byte* row = (byte*)bmd.Scan0 + (y * bmd.Stride);

                    for (int x = 0; x < bmd.Width; x++)
                    {
                        Color curPixel = Color.FromArgb(row[x * PixelSize + 2], row[x * PixelSize + 1], row[x * PixelSize]);

                        array[y, x].belongsToChar = (curPixel.ToArgb() == Color.White.ToArgb()) ? false : true;
                    }
                }
            }
            workBmp.UnlockBits(bmd);


            // Apply thining algoritm
            ApplyZhangSuenAlgo(array, workBmp.Height, workBmp.Width);


            // Project thinning result on the image
            bmd = workBmp.LockBits(new Rectangle(0, 0, workBmp.Width, workBmp.Height), ImageLockMode.ReadWrite,
                                           workBmp.PixelFormat);
            unsafe
            {
                for (int y = 0; y < bmd.Height; y++)
                {
                    byte* row = (byte*)bmd.Scan0 + (y * bmd.Stride);

                    for (int x = 0; x < bmd.Width; x++)
                    {
                        if (array[y, x].belongsToChar) continue;

                        // Background pixel, set to White color
                        row[x * PixelSize + 2] = 255;
                        row[x * PixelSize + 1] = 255;
                        row[x * PixelSize] = 255;
                    }
                }
            }
            workBmp.UnlockBits(bmd);

            return workBmp;
        }

        // http://www.rupj.net/portfolio/docs/skeletonization.pdf
        private static void ApplyZhangSuenAlgo(ImagePixel[,] _array, int _height, int _width)
        {
            while (Zhang_SuenIteration(1, _array, _height, _width))
            {
                if (!Zhang_SuenIteration(2, _array, _height, _width))
                    return;
            }
        }

        private static bool Zhang_SuenIteration(int _iterN, ImagePixel[,] _array, int _height, int _width)
        {
            for (int y = 0; y < _height; y++)
            {
                for (int x = 0; x < _width; x++)
                {
                    _array[y, x].matchNConditions = 0;

                    if (!_array[y, x].belongsToChar) continue;

                    // Check conectivity condition
                    if (GetPixelConectivity(_array, y, x) == 1)
                        _array[y, x].matchNConditions++;

                    // Check neighbouring condition
                    int nb = GetPixelNeighbours(_array, y, x);
                    if (nb >= 2 && nb <= 6)
                        _array[y, x].matchNConditions++;

                    if (_iterN == 1)
                    {
                        if (!_array[y - 1, x].belongsToChar || !_array[y, x + 1].belongsToChar || !_array[y + 1, x].belongsToChar)
                            _array[y, x].matchNConditions++;

                        if (!_array[y, x + 1].belongsToChar || !_array[y + 1, x].belongsToChar || !_array[y, x - 1].belongsToChar)
                            _array[y, x].matchNConditions++;
                    }
                    else
                    {
                        if (!_array[y - 1, x].belongsToChar || !_array[y, x + 1].belongsToChar || !_array[y, x - 1].belongsToChar)
                            _array[y, x].matchNConditions++;

                        if (!_array[y - 1, x].belongsToChar || !_array[y + 1, x].belongsToChar || !_array[y, x - 1].belongsToChar)
                            _array[y, x].matchNConditions++;
                    }
                }
            }

            bool deletedPixels = false;
            for (int y = 0; y < _height; y++)
            {
                for (int x = 0; x < _width; x++)
                {
                    if (_array[y, x].matchNConditions != 4) continue;

                    deletedPixels = true;
                    _array[y, x].belongsToChar = false;
                }
            }

            return deletedPixels;
        }

        private static int GetPixelNeighbours(ImagePixel[,] _array, int _y, int _x)
        {
            int res = 0;
            if (_array[_y - 1, _x].belongsToChar)
                res++;

            if (_array[_y - 1, _x + 1].belongsToChar)
                res++;

            if (_array[_y, _x + 1].belongsToChar)
                res++;

            if (_array[_y + 1, _x + 1].belongsToChar)
                res++;

            if (_array[_y + 1, _x].belongsToChar)
                res++;

            if (_array[_y + 1, _x - 1].belongsToChar)
                res++;

            if (_array[_y, _x - 1].belongsToChar)
                res++;

            if (_array[_y - 1, _x - 1].belongsToChar)
                res++;

            return res;
        }


        private static int GetPixelConectivity(ImagePixel[,] _array, int _y, int _x)
        {
            int res = 0;

            if (!_array[_y - 1, _x].belongsToChar && _array[_y - 1, _x + 1].belongsToChar)
                res++;

            if (!_array[_y - 1, _x + 1].belongsToChar && _array[_y, _x + 1].belongsToChar)
                res++;

            if (!_array[_y, _x + 1].belongsToChar && _array[_y + 1, _x + 1].belongsToChar)
                res++;

            if (!_array[_y + 1, _x + 1].belongsToChar && _array[_y + 1, _x].belongsToChar)
                res++;

            if (!_array[_y + 1, _x].belongsToChar && _array[_y + 1, _x - 1].belongsToChar)
                res++;

            if (!_array[_y + 1, _x - 1].belongsToChar && _array[_y, _x - 1].belongsToChar)
                res++;

            if (!_array[_y, _x - 1].belongsToChar && _array[_y - 1, _x - 1].belongsToChar)
                res++;

            if (!_array[_y - 1, _x - 1].belongsToChar && _array[_y - 1, _x].belongsToChar)
                res++;

            return res;
        }
    }
}
